Micron Document
baubs git

Node / mirrors / respira / commits / efc7129

Commit efc712995b0d90fab876fae289250e630faa9d9b


Parents : bf3e397
Author : Jan-Henrik Bruhn <jan-henrik.bruhn@offis.de>
Date : 2025-12-11T12:31:57+01:00

Move error display to header with clickable popover

- Replace content-pushing error banners with header status indicator
- Add categorized error labels (Python Error, Connection Error, etc.)
- Show detailed error info with solutions in 600px popover on click
- Fix layout shift by always rendering error button (invisible when no error)
- Clear machineError state on disconnect
- Fix TypeScript error in WorkflowStepper ref callback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Changes

4 files changed, 467 insertions(+), 419 deletions(-)

M src/App.tsx +145 -62

Diff

diff --git a/src/App.tsx b/src/App.tsx
index c6a3166..56fc0d5 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,17 +1,16 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useCallback, useRef } from 'react';
import { useBrotherMachine } from './hooks/useBrotherMachine';
import { FileUpload } from './components/FileUpload';
import { PatternCanvas } from './components/PatternCanvas';
import { ProgressMonitor } from './components/ProgressMonitor';
import { WorkflowStepper } from './components/WorkflowStepper';
-import { NextStepGuide } from './components/NextStepGuide';
import { PatternSummaryCard } from './components/PatternSummaryCard';
import { BluetoothDevicePicker } from './components/BluetoothDevicePicker';
import type { PesPatternData } from './utils/pystitchConverter';
import { pyodideLoader } from './utils/pyodideLoader';
-import { hasError } from './utils/errorCodeHelpers';
+import { hasError, getErrorDetails } from './utils/errorCodeHelpers';
import { canDeletePattern, getStateVisualInfo } from './utils/machineStateHelpers';
-import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/solid';
+import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon, InformationCircleIcon } from '@heroicons/react/24/solid';
import './App.css';
function App() {
@@ -22,6 +21,9 @@ function App() {
const [patternOffset, setPatternOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
const [patternUploaded, setPatternUploaded] = useState(false);
const [currentFileName, setCurrentFileName] = useState<string>(''); // Track current pattern filename
+ const [showErrorPopover, setShowErrorPopover] = useState(false);
+ const errorPopoverRef = useRef<HTMLDivElement>(null);
+ const errorButtonRef = useRef<HTMLButtonElement>(null);
// Initialize Pyodide on mount
useEffect(() => {
@@ -37,6 +39,25 @@ function App() {
});
}, []);
+ // Close error popover when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ errorPopoverRef.current &&
+ !errorPopoverRef.current.contains(event.target as Node) &&
+ errorButtonRef.current &&
+ !errorButtonRef.current.contains(event.target as Node)
+ ) {
+ setShowErrorPopover(false);
+ }
+ };
+
+ if (showErrorPopover) {
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }
+ }, [showErrorPopover]);
+
// Auto-load cached pattern when available
const resumedPattern = machine.resumedPattern;
const resumeFileName = machine.resumeFileName;
@@ -142,7 +163,7 @@ function App() {
<ArrowPathIcon className="w-3.5 h-3.5 text-blue-200 animate-spin" title="Auto-refreshing status" />
)}
</div>
- <div className="flex items-center gap-2 mt-1">
+ <div className="flex items-center gap-2 mt-1 min-h-[32px]">
{machine.isConnected ? (
<>
<button
@@ -162,6 +183,122 @@ function App() {
) : (
<p className="text-xs text-blue-200">Not Connected</p>
)}
+
+ {/* Error indicator - always render to prevent layout shift */}
+ <div className="relative">
+ <button
+ ref={errorButtonRef}
+ onClick={() => setShowErrorPopover(!showErrorPopover)}
+ className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 sm:py-1 rounded text-sm font-medium bg-red-500/90 hover:bg-red-600 text-white border border-red-400 transition-all flex-shrink-0 ${
+ (machine.error || pyodideError)
+ ? 'cursor-pointer animate-pulse hover:animate-none'
+ : 'invisible pointer-events-none'
+ }`}
+ title="Click to view error details"
+ aria-label="View error details"
+ disabled={!(machine.error || pyodideError)}
+ >
+ <ExclamationTriangleIcon className="w-3.5 h-3.5 flex-shrink-0" />
+ <span>
+ {(() => {
+ if (pyodideError) return 'Python Error';
+ if (machine.isPairingError) return 'Pairing Required';
+
+ const errorMsg = machine.error || '';
+
+ // Categorize by error message content
+ if (errorMsg.toLowerCase().includes('bluetooth') || errorMsg.toLowerCase().includes('connection')) {
+ return 'Connection Error';
+ }
+ if (errorMsg.toLowerCase().includes('upload')) {
+ return 'Upload Error';
+ }
+ if (errorMsg.toLowerCase().includes('pattern')) {
+ return 'Pattern Error';
+ }
+ if (machine.machineError !== undefined) {
+ return `Machine Error`;
+ }
+
+ // Default fallback
+ return 'Error';
+ })()}
+ </span>
+ </button>
+
+ {/* Error popover */}
+ {showErrorPopover && (machine.error || pyodideError) && (
+ <div
+ ref={errorPopoverRef}
+ className="absolute top-full mt-2 left-0 w-[600px] z-50 animate-fadeIn"
+ role="dialog"
+ aria-label="Error details"
+ >
+ {(() => {
+ const errorDetails = getErrorDetails(machine.machineError);
+ const isPairingError = machine.isPairingError;
+ const errorMsg = pyodideError || machine.error || '';
+ const isInfo = isPairingError || errorDetails?.isInformational;
+
+ const bgColor = isInfo
+ ? 'bg-blue-50 dark:bg-blue-900/95 border-blue-600 dark:border-blue-500'
+ : 'bg-red-50 dark:bg-red-900/95 border-red-600 dark:border-red-500';
+
+ const iconColor = isInfo
+ ? 'text-blue-600 dark:text-blue-400'
+ : 'text-red-600 dark:text-red-400';
+
+ const textColor = isInfo
+ ? 'text-blue-900 dark:text-blue-200'
+ : 'text-red-900 dark:text-red-200';
+
+ const descColor = isInfo
+ ? 'text-blue-800 dark:text-blue-300'
+ : 'text-red-800 dark:text-red-300';
+
+ const listColor = isInfo
+ ? 'text-blue-700 dark:text-blue-300'
+ : 'text-red-700 dark:text-red-300';
+
+ const Icon = isInfo ? InformationCircleIcon : ExclamationTriangleIcon;
+ const title = errorDetails?.title || (isPairingError ? 'Pairing Required' : 'Error');
+
+ return (
+ <div className={`${bgColor} border-l-4 p-4 rounded-lg shadow-xl backdrop-blur-sm`}>
+ <div className="flex items-start gap-3">
+ <Icon className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} />
+ <div className="flex-1">
+ <h3 className={`text-base font-semibold ${textColor} mb-2`}>
+ {title}
+ </h3>
+ <p className={`text-sm ${descColor} mb-3`}>
+ {errorDetails?.description || errorMsg}
+ </p>
+ {errorDetails?.solutions && errorDetails.solutions.length > 0 && (
+ <>
+ <h4 className={`text-sm font-semibold ${textColor} mb-2`}>
+ {isInfo ? 'Steps:' : 'How to Fix:'}
+ </h4>
+ <ol className={`list-decimal list-inside text-sm ${listColor} space-y-1.5`}>
+ {errorDetails.solutions.map((solution, index) => (
+ <li key={index} className="pl-2">{solution}</li>
+ ))}
+ </ol>
+ </>
+ )}
+ {machine.machineError !== undefined && !errorDetails?.isInformational && (
+ <p className={`text-xs ${descColor} mt-3 font-mono`}>
+ Error Code: 0x{machine.machineError.toString(16).toUpperCase().padStart(2, '0')}
+ </p>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+ })()}
+ </div>
+ )}
+ </div>
</div>
</div>
</div>
@@ -173,58 +310,15 @@ function App() {
isConnected={machine.isConnected}
hasPattern={pesData !== null}
patternUploaded={patternUploaded}
+ hasError={hasError(machine.machineError)}
+ errorMessage={machine.error || undefined}
+ errorCode={machine.machineError}
/>
</div>
</div>
</header>
<div className="flex-1 p-4 sm:p-5 lg:p-6 w-full overflow-y-auto lg:overflow-hidden flex flex-col">
- {/* Global errors */}
- {machine.error && (
- <div className={`px-6 py-4 rounded-lg border-l-4 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn ${
- machine.isPairingError
- ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-200 border-blue-600 dark:border-blue-500'
- : 'bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-200 border-red-600 dark:border-red-500'
- }`}>
- <div className="flex items-center gap-3">
- <svg className={`w-5 h-5 flex-shrink-0 ${machine.isPairingError ? 'text-blue-600 dark:text-blue-400' : 'text-red-600 dark:text-red-400'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
- {machine.isPairingError ? (
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
- ) : (
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
- )}
- </svg>
- <div className="flex-1">
- <div className="font-semibold mb-1">{machine.isPairingError ? 'Pairing Required' : 'Error'}</div>
- <div className="text-sm">{machine.error}</div>
- </div>
- </div>
- </div>
- )}
- {pyodideError && (
- <div className="bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-200 px-6 py-4 rounded-lg border-l-4 border-red-600 dark:border-red-500 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn">
- <div className="flex items-center gap-2">
- <svg className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
- <div>
- <strong className="font-semibold">Python Error:</strong> {pyodideError}
- </div>
- </div>
- </div>
- )}
- {!pyodideReady && !pyodideError && (
- <div className="bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-200 px-6 py-4 rounded-lg border-l-4 border-blue-600 dark:border-blue-500 mb-6 shadow-md animate-fadeIn">
- <div className="flex items-center gap-3">
- <svg className="w-5 h-5 animate-spin text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24">
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
- </svg>
- <span className="font-medium">Initializing Python environment...</span>
- </div>
- </div>
- )}
-
<div className="flex-1 grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
{/* Left Column - Controls */}
<div className="flex flex-col gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
@@ -362,17 +456,6 @@ function App() {
{/* Bluetooth Device Picker (Electron only) */}
<BluetoothDevicePicker />
</div>
-
- {/* Next Step Guide - Fixed floating overlay */}
- <NextStepGuide
- machineStatus={machine.machineStatus}
- isConnected={machine.isConnected}
- hasPattern={pesData !== null}
- patternUploaded={patternUploaded}
- hasError={hasError(machine.machineError)}
- errorMessage={machine.error || undefined}
- errorCode={machine.machineError}
- />
</div>
);
}

diff --git a/src/components/NextStepGuide.tsx b/src/components/NextStepGuide.tsx
deleted file mode 100644
index fc07e89..0000000
--- a/src/components/NextStepGuide.tsx
+++ /dev/null
@@ -1,354 +0,0 @@
-import { useState, useEffect } from 'react';
-import { InformationCircleIcon, ExclamationTriangleIcon, XMarkIcon } from '@heroicons/react/24/solid';
-import { MachineStatus } from '../types/machine';
-import { getErrorDetails } from '../utils/errorCodeHelpers';
-
-interface NextStepGuideProps {
- machineStatus: MachineStatus;
- isConnected: boolean;
- hasPattern: boolean;
- patternUploaded: boolean;
- hasError: boolean;
- errorMessage?: string;
- errorCode?: number;
-}
-
-export function NextStepGuide({
- machineStatus,
- isConnected,
- hasPattern,
- patternUploaded,
- hasError,
- errorMessage,
- errorCode
-}: NextStepGuideProps) {
- const [isExpanded, setIsExpanded] = useState(true);
-
- // Expand when state changes
- useEffect(() => {
- setIsExpanded(true);
- }, [machineStatus, isConnected, hasPattern, patternUploaded, hasError]);
-
- // Render guide content based on state
- const renderContent = () => {
- // Don't show if there's an error - show detailed error guidance instead
- if (hasError) {
- const errorDetails = getErrorDetails(errorCode);
-
- // Check if this is informational (like initialization steps) vs a real error
- if (errorDetails?.isInformational) {
- return (
- <div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">
- {errorDetails.title}
- </h3>
- <p className="text-sm text-blue-800 dark:text-blue-300 mb-3">
- {errorDetails.description}
- </p>
- {errorDetails.solutions && errorDetails.solutions.length > 0 && (
- <>
- <h4 className="text-sm font-semibold text-blue-900 dark:text-blue-200 mb-2">Steps:</h4>
- <ol className="list-decimal list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1.5">
- {errorDetails.solutions.map((solution, index) => (
- <li key={index} className="pl-2">{solution}</li>
- ))}
- </ol>
- </>
- )}
- </div>
- </div>
- </div>
- );
- }
-
- // Regular error display for actual errors
- return (
- <div className="bg-red-50 dark:bg-red-900/95 border-l-4 border-red-600 dark:border-red-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <ExclamationTriangleIcon className="w-6 h-6 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-red-900 dark:text-red-200 mb-2">
- {errorDetails?.title || 'Error Occurred'}
- </h3>
- <p className="text-sm text-red-800 dark:text-red-300 mb-3">
- {errorDetails?.description || errorMessage || 'An error occurred. Please check the machine and try again.'}
- </p>
- {errorDetails?.solutions && errorDetails.solutions.length > 0 && (
- <>
- <h4 className="text-sm font-semibold text-red-900 dark:text-red-200 mb-2">How to Fix:</h4>
- <ol className="list-decimal list-inside text-sm text-red-700 dark:text-red-300 space-y-1.5">
- {errorDetails.solutions.map((solution, index) => (
- <li key={index} className="pl-2">{solution}</li>
- ))}
- </ol>
- </>
- )}
- {errorCode !== undefined && (
- <p className="text-xs text-red-600 dark:text-red-400 mt-3 font-mono">
- Error Code: 0x{errorCode.toString(16).toUpperCase().padStart(2, '0')}
- </p>
- )}
- </div>
- </div>
- </div>
- );
- }
-
- // Determine what to show based on current state
- if (!isConnected) {
- return (
- <div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 1: Connect to Machine</h3>
- <p className="text-sm text-blue-800 dark:text-blue-300 mb-3">To get started, connect to your Brother embroidery machine via Bluetooth.</p>
- <ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
- <li>Make sure your machine is powered on</li>
- <li>Enable Bluetooth on your machine</li>
- <li>Click the "Connect to Machine" button below</li>
- </ul>
- </div>
- </div>
- </div>
- );
- }
-
- if (!hasPattern) {
- return (
- <div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 2: Load Your Pattern</h3>
- <p className="text-sm text-blue-800 dark:text-blue-300 mb-3">Choose a PES embroidery file from your computer to preview and upload.</p>
- <ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
- <li>Click "Choose PES File" in the Pattern File section</li>
- <li>Select your embroidery design (.pes file)</li>
- <li>Review the pattern preview on the right</li>
- <li>You can drag the pattern to adjust its position</li>
- </ul>
- </div>
- </div>
- </div>
- );
- }
-
- if (!patternUploaded) {
- return (
- <div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 3: Upload Pattern to Machine</h3>
- <p className="text-sm text-blue-800 dark:text-blue-300 mb-3">Send your pattern to the embroidery machine to prepare for sewing.</p>
- <ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
- <li>Review the pattern preview to ensure it's positioned correctly</li>
- <li>Check the pattern size matches your hoop</li>
- <li>Click "Upload to Machine" when ready</li>
- <li>Wait for the upload to complete (this may take a minute)</li>
- </ul>
- </div>
- </div>
- </div>
- );
- }
-
- // Pattern is uploaded, guide based on machine status
- switch (machineStatus) {
- case MachineStatus.IDLE:
- return (
- <div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 4: Start Mask Trace</h3>
- <p className="text-sm text-blue-800 dark:text-blue-300 mb-3">The mask trace helps the machine understand the pattern boundaries.</p>
- <ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
- <li>Click "Start Mask Trace" button in the Sewing Progress section</li>
- <li>The machine will trace the pattern outline</li>
- <li>This ensures the hoop is positioned correctly</li>
- </ul>
- </div>
- </div>
- </div>
- );
-
- case MachineStatus.MASK_TRACE_LOCK_WAIT:
- return (
- <div className="bg-yellow-50 dark:bg-yellow-900/95 border-l-4 border-yellow-600 dark:border-yellow-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Machine Action Required</h3>
- <p className="text-sm text-yellow-800 dark:text-yellow-300 mb-3">The machine is ready to trace the pattern outline.</p>
- <ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
- <li><strong>Press the button on your machine</strong> to confirm and start the mask trace</li>
- <li>Ensure the hoop is properly attached</li>
- <li>Make sure the needle area is clear</li>
- </ul>
- </div>
- </div>
- </div>
- );
-
- case MachineStatus.MASK_TRACING:
- return (
- <div className="bg-cyan-50 dark:bg-cyan-900/95 border-l-4 border-cyan-600 dark:border-cyan-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-cyan-600 dark:text-cyan-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-cyan-900 dark:text-cyan-200 mb-2">Mask Trace In Progress</h3>
- <p className="text-sm text-cyan-800 dark:text-cyan-300 mb-3">The machine is tracing the pattern boundary. Please wait...</p>
- <ul className="list-disc list-inside text-sm text-cyan-700 dark:text-cyan-300 space-y-1">
- <li>Watch the machine trace the outline</li>
- <li>Verify the pattern fits within your hoop</li>
- <li>Do not interrupt the machine</li>
- </ul>
- </div>
- </div>
- </div>
- );
-
- case MachineStatus.MASK_TRACE_COMPLETE:
- case MachineStatus.SEWING_WAIT:
- return (
- <div className="bg-green-50 dark:bg-green-900/95 border-l-4 border-green-600 dark:border-green-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-green-900 dark:text-green-200 mb-2">Step 5: Ready to Sew!</h3>
- <p className="text-sm text-green-800 dark:text-green-300 mb-3">The machine is ready to begin embroidering your pattern.</p>
- <ul className="list-disc list-inside text-sm text-green-700 dark:text-green-300 space-y-1">
- <li>Verify your thread colors are correct</li>
- <li>Ensure the fabric is properly hooped</li>
- <li>Click "Start Sewing" when ready</li>
- </ul>
- </div>
- </div>
- </div>
- );
-
- case MachineStatus.SEWING:
- return (
- <div className="bg-cyan-50 dark:bg-cyan-900/95 border-l-4 border-cyan-600 dark:border-cyan-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-cyan-600 dark:text-cyan-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-cyan-900 dark:text-cyan-200 mb-2">Step 6: Sewing In Progress</h3>
- <p className="text-sm text-cyan-800 dark:text-cyan-300 mb-3">Your embroidery is being stitched. Monitor the progress below.</p>
- <ul className="list-disc list-inside text-sm text-cyan-700 dark:text-cyan-300 space-y-1">
- <li>Watch the progress bar and current stitch count</li>
- <li>The machine will pause when a color change is needed</li>
- <li>Do not leave the machine unattended</li>
- </ul>
- </div>
- </div>
- </div>
- );
-
- case MachineStatus.COLOR_CHANGE_WAIT:
- return (
- <div className="bg-yellow-50 dark:bg-yellow-900/95 border-l-4 border-yellow-600 dark:border-yellow-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Thread Change Required</h3>
- <p className="text-sm text-yellow-800 dark:text-yellow-300 mb-3">The machine needs a different thread color to continue.</p>
- <ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
- <li>Check the color blocks section to see which thread is needed</li>
- <li>Change to the correct thread color</li>
- <li><strong>Press the button on your machine</strong> to resume sewing</li>
- </ul>
- </div>
- </div>
- </div>
- );
-
- case MachineStatus.PAUSE:
- case MachineStatus.STOP:
- case MachineStatus.SEWING_INTERRUPTION:
- return (
- <div className="bg-yellow-50 dark:bg-yellow-900/95 border-l-4 border-yellow-600 dark:border-yellow-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Sewing Paused</h3>
- <p className="text-sm text-yellow-800 dark:text-yellow-300 mb-3">The embroidery has been paused or interrupted.</p>
- <ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
- <li>Check if everything is okay with the machine</li>
- <li>Click "Resume Sewing" when ready to continue</li>
- <li>The machine will pick up where it left off</li>
- </ul>
- </div>
- </div>
- </div>
- );
-
- case MachineStatus.SEWING_COMPLETE:
- return (
- <div className="bg-green-50 dark:bg-green-900/95 border-l-4 border-green-600 dark:border-green-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
- <div className="flex items-start gap-3">
- <InformationCircleIcon className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
- <div className="flex-1">
- <h3 className="text-base font-semibold text-green-900 dark:text-green-200 mb-2">Step 7: Embroidery Complete!</h3>
- <p className="text-sm text-green-800 dark:text-green-300 mb-3">Your embroidery is finished. Great work!</p>
- <ul className="list-disc list-inside text-sm text-green-700 dark:text-green-300 space-y-1">
- <li>Remove the hoop from the machine</li>
- <li>Press the Accept button on the machine</li>
- <li>Carefully remove your finished embroidery</li>
- <li>Trim any jump stitches or loose threads</li>
- <li>Click "Delete Pattern" to start a new project</li>
- </ul>
- </div>
- </div>
- </div>
- );
-
- default:
- return null;
- }
- };
-
- // Render floating overlay
- return (
- <>
- {/* Collapsed state - small info button in bottom-right */}
- {!isExpanded && (
- <div className="fixed bottom-4 right-4 z-50 animate-fadeIn">
- <button
- onClick={() => setIsExpanded(true)}
- className="w-12 h-12 bg-blue-600 dark:bg-blue-700 hover:bg-blue-700 dark:hover:bg-blue-600 text-white rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110 animate-pulse"
- aria-label="Show next step guide"
- title="Show next step guide"
- >
- <InformationCircleIcon className="w-7 h-7" />
- </button>
- </div>
- )}
-
- {/* Expanded state - floating overlay */}
- {isExpanded && (
- <div className="fixed bottom-4 right-4 z-50 w-[90vw] max-w-[400px] max-h-[40vh] overflow-y-auto animate-slideInRight">
- <div className="relative rounded-lg overflow-hidden shadow-xl">
- {/* Close button */}
- <button
- onClick={() => setIsExpanded(false)}
- className="absolute top-2 right-2 z-10 w-6 h-6 bg-white/90 dark:bg-gray-800/90 hover:bg-white dark:hover:bg-gray-700 rounded-full flex items-center justify-center transition-colors shadow-md"
- aria-label="Minimize guide"
- title="Minimize guide"
- >
- <XMarkIcon className="w-4 h-4 text-gray-700 dark:text-gray-300" />
- </button>
-
- {/* Content */}
- {renderContent()}
- </div>
- </div>
- )}
- </>
- );
-}

diff --git a/src/components/WorkflowStepper.tsx b/src/components/WorkflowStepper.tsx
index 1f03ebc..ed4df8c 100644
--- a/src/components/WorkflowStepper.tsx
+++ b/src/components/WorkflowStepper.tsx
@@ -1,11 +1,16 @@
-import { CheckCircleIcon } from '@heroicons/react/24/solid';
+import { useState, useRef, useEffect } from 'react';
+import { CheckCircleIcon, InformationCircleIcon, ExclamationTriangleIcon } from '@heroicons/react/24/solid';
import { MachineStatus } from '../types/machine';
+import { getErrorDetails } from '../utils/errorCodeHelpers';
interface WorkflowStepperProps {
machineStatus: MachineStatus;
isConnected: boolean;
hasPattern: boolean;
patternUploaded: boolean;
+ hasError?: boolean;
+ errorMessage?: string;
+ errorCode?: number;
}
interface Step {
@@ -24,6 +29,183 @@ const steps: Step[] = [
{ id: 7, label: 'Complete', description: 'Finish and remove' },
];
+// Helper function to get guide content for a step
+function getGuideContent(
+ stepId: number,
+ machineStatus: MachineStatus,
+ hasError: boolean,
+ errorCode?: number,
+ errorMessage?: string
+) {
+ // Check for errors first
+ if (hasError) {
+ const errorDetails = getErrorDetails(errorCode);
+
+ if (errorDetails?.isInformational) {
+ return {
+ type: 'info' as const,
+ title: errorDetails.title,
+ description: errorDetails.description,
+ items: errorDetails.solutions || []
+ };
+ }
+
+ return {
+ type: 'error' as const,
+ title: errorDetails?.title || 'Error Occurred',
+ description: errorDetails?.description || errorMessage || 'An error occurred. Please check the machine and try again.',
+ items: errorDetails?.solutions || [],
+ errorCode
+ };
+ }
+
+ // Return content based on step
+ switch (stepId) {
+ case 1:
+ return {
+ type: 'info' as const,
+ title: 'Step 1: Connect to Machine',
+ description: 'To get started, connect to your Brother embroidery machine via Bluetooth.',
+ items: [
+ 'Make sure your machine is powered on',
+ 'Enable Bluetooth on your machine',
+ 'Click the "Connect to Machine" button below'
+ ]
+ };
+
+ case 2:
+ return {
+ type: 'info' as const,
+ title: 'Step 2: Load Your Pattern',
+ description: 'Choose a PES embroidery file from your computer to preview and upload.',
+ items: [
+ 'Click "Choose PES File" in the Pattern File section',
+ 'Select your embroidery design (.pes file)',
+ 'Review the pattern preview on the right',
+ 'You can drag the pattern to adjust its position'
+ ]
+ };
+
+ case 3:
+ return {
+ type: 'info' as const,
+ title: 'Step 3: Upload Pattern to Machine',
+ description: 'Send your pattern to the embroidery machine to prepare for sewing.',
+ items: [
+ 'Review the pattern preview to ensure it\'s positioned correctly',
+ 'Check the pattern size matches your hoop',
+ 'Click "Upload to Machine" when ready',
+ 'Wait for the upload to complete (this may take a minute)'
+ ]
+ };
+
+ case 4:
+ // Check machine status for substates
+ if (machineStatus === MachineStatus.MASK_TRACE_LOCK_WAIT) {
+ return {
+ type: 'warning' as const,
+ title: 'Machine Action Required',
+ description: 'The machine is ready to trace the pattern outline.',
+ items: [
+ 'Press the button on your machine to confirm and start the mask trace',
+ 'Ensure the hoop is properly attached',
+ 'Make sure the needle area is clear'
+ ]
+ };
+ }
+ if (machineStatus === MachineStatus.MASK_TRACING) {
+ return {
+ type: 'progress' as const,
+ title: 'Mask Trace In Progress',
+ description: 'The machine is tracing the pattern boundary. Please wait...',
+ items: [
+ 'Watch the machine trace the outline',
+ 'Verify the pattern fits within your hoop',
+ 'Do not interrupt the machine'
+ ]
+ };
+ }
+ return {
+ type: 'info' as const,
+ title: 'Step 4: Start Mask Trace',
+ description: 'The mask trace helps the machine understand the pattern boundaries.',
+ items: [
+ 'Click "Start Mask Trace" button in the Sewing Progress section',
+ 'The machine will trace the pattern outline',
+ 'This ensures the hoop is positioned correctly'
+ ]
+ };
+
+ case 5:
+ return {
+ type: 'success' as const,
+ title: 'Step 5: Ready to Sew!',
+ description: 'The machine is ready to begin embroidering your pattern.',
+ items: [
+ 'Verify your thread colors are correct',
+ 'Ensure the fabric is properly hooped',
+ 'Click "Start Sewing" when ready'
+ ]
+ };
+
+ case 6:
+ // Check for substates
+ if (machineStatus === MachineStatus.COLOR_CHANGE_WAIT) {
+ return {
+ type: 'warning' as const,
+ title: 'Thread Change Required',
+ description: 'The machine needs a different thread color to continue.',
+ items: [
+ 'Check the color blocks section to see which thread is needed',
+ 'Change to the correct thread color',
+ 'Press the button on your machine to resume sewing'
+ ]
+ };
+ }
+ if (machineStatus === MachineStatus.PAUSE ||
+ machineStatus === MachineStatus.STOP ||
+ machineStatus === MachineStatus.SEWING_INTERRUPTION) {
+ return {
+ type: 'warning' as const,
+ title: 'Sewing Paused',
+ description: 'The embroidery has been paused or interrupted.',
+ items: [
+ 'Check if everything is okay with the machine',
+ 'Click "Resume Sewing" when ready to continue',
+ 'The machine will pick up where it left off'
+ ]
+ };
+ }
+ return {
+ type: 'progress' as const,
+ title: 'Step 6: Sewing In Progress',
+ description: 'Your embroidery is being stitched. Monitor the progress below.',
+ items: [
+ 'Watch the progress bar and current stitch count',
+ 'The machine will pause when a color change is needed',
+ 'Do not leave the machine unattended'
+ ]
+ };
+
+ case 7:
+ return {
+ type: 'success' as const,
+ title: 'Step 7: Embroidery Complete!',
+ description: 'Your embroidery is finished. Great work!',
+ items: [
+ 'Remove the hoop from the machine',
+ 'Press the Accept button on the machine',
+ 'Carefully remove your finished embroidery',
+ 'Trim any jump stitches or loose threads',
+ 'Click "Delete Pattern" to start a new project'
+ ]
+ };
+
+ default:
+ return null;
+ }
+}
+
function getCurrentStep(machineStatus: MachineStatus, isConnected: boolean, hasPattern: boolean, patternUploaded: boolean): number {
if (!isConnected) return 1;
if (!hasPattern) return 2;
@@ -55,8 +237,53 @@ function getCurrentStep(machineStatus: MachineStatus, isConnected: boolean, hasP
}
}
-export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patternUploaded }: WorkflowStepperProps) {
+export function WorkflowStepper({
+ machineStatus,
+ isConnected,
+ hasPattern,
+ patternUploaded,
+ hasError = false,
+ errorMessage,
+ errorCode
+}: WorkflowStepperProps) {
const currentStep = getCurrentStep(machineStatus, isConnected, hasPattern, patternUploaded);
+ const [showPopover, setShowPopover] = useState(false);
+ const [popoverStep, setPopoverStep] = useState<number | null>(null);
+ const popoverRef = useRef<HTMLDivElement>(null);
+ const stepRefs = useRef<{ [key: number]: HTMLDivElement | null }>({});
+
+ // Close popover when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
+ // Check if click was on a step circle
+ const clickedStep = Object.values(stepRefs.current).find(ref =>
+ ref?.contains(event.target as Node)
+ );
+ if (!clickedStep) {
+ setShowPopover(false);
+ }
+ }
+ };
+
+ if (showPopover) {
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }
+ }, [showPopover]);
+
+ const handleStepClick = (stepId: number) => {
+ // Only allow clicking on current step or earlier completed steps
+ if (stepId <= currentStep) {
+ if (showPopover && popoverStep === stepId) {
+ setShowPopover(false);
+ setPopoverStep(null);
+ } else {
+ setPopoverStep(stepId);
+ setShowPopover(true);
+ }
+ }
+ };
return (
<div className="relative max-w-5xl mx-auto mt-2 lg:mt-4" role="navigation" aria-label="Workflow progress">
@@ -94,13 +321,19 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
>
{/* Step circle */}
<div
+ ref={(el) => { stepRefs.current[step.id] = el; }}
+ onClick={() => handleStepClick(step.id)}
className={`
w-8 h-8 lg:w-10 lg:h-10 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-300 border-2 shadow-md
+ ${step.id <= currentStep ? 'cursor-pointer hover:scale-110' : 'cursor-not-allowed'}
${isComplete ? 'bg-green-500 dark:bg-green-600 border-green-400 dark:border-green-500 text-white shadow-green-500/30 dark:shadow-green-600/30' : ''}
${isCurrent ? 'bg-blue-600 dark:bg-blue-700 border-blue-500 dark:border-blue-600 text-white scale-105 lg:scale-110 shadow-blue-600/40 dark:shadow-blue-700/40 ring-2 ring-blue-300 dark:ring-blue-500 ring-offset-2 dark:ring-offset-gray-900' : ''}
${isUpcoming ? 'bg-blue-700 dark:bg-blue-800 border-blue-500/30 dark:border-blue-600/30 text-blue-200/70 dark:text-blue-300/70' : ''}
+ ${showPopover && popoverStep === step.id ? 'ring-4 ring-white dark:ring-gray-800' : ''}
`}
- aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}`}
+ aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}. Click for details.`}
+ role="button"
+ tabIndex={step.id <= currentStep ? 0 : -1}
>
{isComplete ? (
<CheckCircleIcon className="w-5 h-5 lg:w-6 lg:h-6" aria-hidden="true" />
@@ -121,6 +354,91 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
);
})}
</div>
+
+ {/* Popover */}
+ {showPopover && popoverStep !== null && stepRefs.current[popoverStep] && (
+ <div
+ ref={popoverRef}
+ className="absolute top-full mt-4 left-1/2 transform -translate-x-1/2 w-full max-w-xl z-50 animate-fadeIn"
+ role="dialog"
+ aria-label="Step guidance"
+ >
+ {(() => {
+ const content = getGuideContent(popoverStep, machineStatus, hasError, errorCode, errorMessage);
+ if (!content) return null;
+
+ const colorClasses = {
+ info: 'bg-blue-50 dark:bg-blue-900/95 border-blue-600 dark:border-blue-500',
+ success: 'bg-green-50 dark:bg-green-900/95 border-green-600 dark:border-green-500',
+ warning: 'bg-yellow-50 dark:bg-yellow-900/95 border-yellow-600 dark:border-yellow-500',
+ error: 'bg-red-50 dark:bg-red-900/95 border-red-600 dark:border-red-500',
+ progress: 'bg-cyan-50 dark:bg-cyan-900/95 border-cyan-600 dark:border-cyan-500'
+ };
+
+ const iconColorClasses = {
+ info: 'text-blue-600 dark:text-blue-400',
+ success: 'text-green-600 dark:text-green-400',
+ warning: 'text-yellow-600 dark:text-yellow-400',
+ error: 'text-red-600 dark:text-red-400',
+ progress: 'text-cyan-600 dark:text-cyan-400'
+ };
+
+ const textColorClasses = {
+ info: 'text-blue-900 dark:text-blue-200',
+ success: 'text-green-900 dark:text-green-200',
+ warning: 'text-yellow-900 dark:text-yellow-200',
+ error: 'text-red-900 dark:text-red-200',
+ progress: 'text-cyan-900 dark:text-cyan-200'
+ };
+
+ const descColorClasses = {
+ info: 'text-blue-800 dark:text-blue-300',
+ success: 'text-green-800 dark:text-green-300',
+ warning: 'text-yellow-800 dark:text-yellow-300',
+ error: 'text-red-800 dark:text-red-300',
+ progress: 'text-cyan-800 dark:text-cyan-300'
+ };
+
+ const listColorClasses = {
+ info: 'text-blue-700 dark:text-blue-300',
+ success: 'text-green-700 dark:text-green-300',
+ warning: 'text-yellow-700 dark:text-yellow-300',
+ error: 'text-red-700 dark:text-red-300',
+ progress: 'text-cyan-700 dark:text-cyan-300'
+ };
+
+ const Icon = content.type === 'error' ? ExclamationTriangleIcon : InformationCircleIcon;
+
+ return (
+ <div className={`${colorClasses[content.type]} border-l-4 p-4 rounded-lg shadow-xl backdrop-blur-sm`}>
+ <div className="flex items-start gap-3">
+ <Icon className={`w-6 h-6 ${iconColorClasses[content.type]} flex-shrink-0 mt-0.5`} />
+ <div className="flex-1">
+ <h3 className={`text-base font-semibold ${textColorClasses[content.type]} mb-2`}>
+ {content.title}
+ </h3>
+ <p className={`text-sm ${descColorClasses[content.type]} mb-3`}>
+ {content.description}
+ </p>
+ {content.items && content.items.length > 0 && (
+ <ul className={`list-disc list-inside text-sm ${listColorClasses[content.type]} space-y-1`}>
+ {content.items.map((item, index) => (
+ <li key={index} className="pl-2" dangerouslySetInnerHTML={{ __html: item.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') }} />
+ ))}
+ </ul>
+ )}
+ {content.type === 'error' && content.errorCode !== undefined && (
+ <p className={`text-xs ${descColorClasses[content.type]} mt-3 font-mono`}>
+ Error Code: 0x{content.errorCode.toString(16).toUpperCase().padStart(2, '0')}
+ </p>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+ })()}
+ </div>
+ )}
</div>
);
}

diff --git a/src/hooks/useBrotherMachine.ts b/src/hooks/useBrotherMachine.ts
index b3e5ad2..096f465 100644
--- a/src/hooks/useBrotherMachine.ts
+++ b/src/hooks/useBrotherMachine.ts
@@ -152,6 +152,7 @@ export function useBrotherMachine() {
setPatternInfo(null);
setSewingProgress(null);
setError(null);
+ setMachineError(SewingMachineError.None);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to disconnect");
}

Served by rngit 1.3.3 - Generated in 0.07s